﻿/* ------------------------------------------------------------------------ */
/* TheNewCoders Scripting (TNCScripting) v1.0                               */
/* ------------------------------------------------------------------------ */
/* A scripting engine for C# that is even compatible with XNA for Xbox360.  */
/* Copyright (C) 2010 Prageeth Silva <prageeth@thenewcoders.com>            */
/*                                                                          */
/* This program is free software; you can redistribute it and/or            */
/* modify it under the terms of the GNU General Public License              */
/* as published by the Free Software Foundation; either version 2           */
/* of the License, or (at your option) any later version.                   */
/*                                                                          */
/* This program is distributed in the hope that it will be useful,          */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of           */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the            */
/* GNU General Public License for more details.                             */
/*                                                                          */
/* You should have received a copy of the GNU General Public License        */
/* along with this program; if not, write to the Free Software              */
/* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,               */
/* MA  02110-1301, USA. OR visit <http://www.gnu.org/licenses/gpl-2.0.html> */
/* ------------------------------------------------------------------------ */

using System;
using System.Collections.Generic;
using System.Reflection;

namespace TNCScripting
{

    internal abstract class Cmd
    {

        protected const string OperationError = "Operation '{0}' error between objects '{1}' and '{2}'.";

        //public Cmd Parent = null;
        public List<Cmd> Children = new List<Cmd>();

        public CmdIf If = null;
        public virtual bool Execute
        {
            get { return ((this.If == null) || this.If.Evaluate); }
        }

        public abstract int Eval(ScriptFunction sfunc);

        public void AddChild(Cmd c)
        {
            this.Children.Add(c);
            //c.Parent = this;
        }

        protected void RemoveFromTemp(ScriptFunction sfunc, int i)
        {
            for (int j = 0; j < i; j++)
            {
                sfunc.Temps.Pop();
            }
        }

    }

    internal class CmdValue : Cmd
    {

        public object Value = null;
        public bool Var = false;
        public string Name = string.Empty;

        public CmdValue(object Value)
        {
            this.Value = Value;
        }

        public CmdValue(bool var, string name)
        {
            this.Var = var;
            this.Name = name;
        }

        public override int Eval(ScriptFunction sfunc)
        {
            if (this.Var)
            {
                object o;
                if (sfunc.Objects.TryGetValue(this.Name, out o))
                {
                    sfunc.Temps.Push(o);
                    return 1;
                }
                throw new Exception(string.Format("Variable object '{0}' cannote be found on the stack.", this.Name));
            }
            else
            {
                sfunc.Temps.Push(this.Value);
                return 1;
            }
        }

    }

    internal class CmdExpr : Cmd
    {

        private static Type BaseType = typeof(decimal);
        private enum Operation
        {
            NoOp,
            Add,
            Subtract,
            Divide,
            Multiply,
            Mod
        }

        private Operation Op = Operation.NoOp;

        public CmdExpr(string op, Cmd left, Cmd right)
        {
            this.Op = this.GetOperation(op);
            this.Children.Add(left);
            this.Children.Add(right);
        }

        private Operation GetOperation(string op)
        {
            switch (op)
            {
                case "+":
                    return Operation.Add;
                case "-":
                    return Operation.Subtract;
                case "/":
                    return Operation.Divide;
                case "*":
                    return Operation.Multiply;
                case "%":
                    return Operation.Mod;
                default:
                    return Operation.NoOp;
            }
        }

        public override int Eval(ScriptFunction sfunc)
        {
            int i = 0;
            foreach (Cmd c in this.Children)
            {
                i += c.Eval(sfunc);
            }
            if (i == 2)
            {

                // pick the items from stack in reverse order
                object tmp2 = sfunc.Temps.Pop();
                object tmp1 = sfunc.Temps.Pop();

                switch (this.Op)
                {

                    case Operation.Add:
                        sfunc.Temps.Push(this.OperationAdd(tmp1, tmp2));
                        return 1;

                    case Operation.Subtract:
                        sfunc.Temps.Push(this.OperationSubtract(tmp1, tmp2));
                        return 1;

                    case Operation.Divide:
                        sfunc.Temps.Push(this.OperationDivide(tmp1, tmp2));
                        return 1;

                    case Operation.Multiply:
                        sfunc.Temps.Push(this.OperationMultiply(tmp1, tmp2));
                        return 1;

                    case Operation.Mod:
                        sfunc.Temps.Push(this.OperationMod(tmp1, tmp2));
                        return 1;

                    default:
                        return 0;

                }

            }
            else
            {
                return i;
            }

        }

        private Type GetTopType(object t1, object t2)
        {
            if (t1 is decimal || t2 is decimal) { return typeof(decimal); }
            if (t1 is double || t2 is double) { return typeof(double); }
            if (t1 is float || t2 is float) { return typeof(float); }
            if (t1 is ulong || t2 is ulong) { return typeof(ulong); }
            if (t1 is long || t2 is int) { return typeof(long); }
            if (t1 is uint || t2 is uint) { return typeof(uint); }
            if (t1 is int || t2 is int) { return typeof(int); }
            if (t1 is ushort || t2 is ushort) { return typeof(ushort); }
            if (t1 is short || t2 is short) { return typeof(short); }
            if (t1 is byte || t2 is byte) { return typeof(byte); }
            if (t1 is sbyte || t2 is sbyte) { return typeof(sbyte); }
            if (t1 is char || t2 is char) { return typeof(char); }
            return typeof(decimal);
        }
                
        private object OperationAdd(object t1, object t2)
        {
            if (t1 is string || t2 is string)
            {
                return (t1.ToString() + t2.ToString());
            }
            else
            {
                try
                {
                    decimal d1 = (decimal)Convert.ChangeType(t1, CmdExpr.BaseType);
                    decimal d2 = (decimal)Convert.ChangeType(t2, CmdExpr.BaseType);
                    return Convert.ChangeType(d1 + d2, this.GetTopType(t1, t2));
                }
                catch { }
            }
            throw new Exception(string.Format(Cmd.OperationError, "+", t1, t2));
        }

        private object OperationSubtract(object t1, object t2)
        {
            try
            {
                decimal d1 = (decimal)Convert.ChangeType(t1, CmdExpr.BaseType);
                decimal d2 = (decimal)Convert.ChangeType(t2, CmdExpr.BaseType);
                return Convert.ChangeType(d1 - d2, this.GetTopType(t1, t2));              
            }
            catch
            {
                throw new Exception(string.Format(Cmd.OperationError, "-", t1, t2));
            }
        }

        private object OperationDivide(object t1, object t2)
        {
            try
            {
                decimal d1 = (decimal)Convert.ChangeType(t1, CmdExpr.BaseType);
                decimal d2 = (decimal)Convert.ChangeType(t2, CmdExpr.BaseType);
                return Convert.ChangeType(d1 / d2, this.GetTopType(t1, t2));
            }
            catch
            {
                throw new Exception(string.Format(Cmd.OperationError, "/", t1, t2));
            }
        }

        private object OperationMultiply(object t1, object t2)
        {
            try
            {
                decimal d1 = (decimal)Convert.ChangeType(t1, CmdExpr.BaseType);
                decimal d2 = (decimal)Convert.ChangeType(t2, CmdExpr.BaseType);
                return Convert.ChangeType(d1 * d2, this.GetTopType(t1, t2));
            }
            catch
            {
                throw new Exception(string.Format(Cmd.OperationError, "*", t1, t2));
            }
        }

        private object OperationMod(object t1, object t2)
        {
            try
            {
                decimal d1 = (decimal)Convert.ChangeType(t1, CmdExpr.BaseType);
                decimal d2 = (decimal)Convert.ChangeType(t2, CmdExpr.BaseType);
                return Convert.ChangeType(d1 % d2, this.GetTopType(t1, t2));
            }
            catch
            {
                throw new Exception(string.Format(Cmd.OperationError, "%", t1, t2));
            }
        }

    }

    internal class CmdMethod : Cmd
    {

        public List<string> Names = null;

        public CmdMethod(List<string> names)
        {
            this.Names = names;
        }

        public override int Eval(ScriptFunction sfunc)
        {
            int i = 0;
            foreach (Cmd c in this.Children)
            {
                i += c.Eval(sfunc);
            }
            if (i == this.Children.Count)
            {
                object[] param = new object[i];
                Type[] types = new Type[i];
                int idx;
                for (int j = 0; j < i; j++)
                {
                    idx = i - j - 1;
                    param[idx] = sfunc.Temps.Pop();
                    types[idx] = param[idx].GetType();
                }
                i = this.InvokeMethod(sfunc, types, param);
            }
            return i;
        }

        private int InvokeMethod(ScriptFunction sfunc, Type[] argstypes, object[] param)
        {

            string curr = this.Names[0];
            object o;
            if (sfunc.Objects.TryGetValue(curr, out o))
            {

                if (this.Names.Count > 1)
                {

                    object obj = o;

                    int len = this.Names.Count - 1;
                    for (int i = 1; i < this.Names.Count; i++)
                    {
                        curr = this.Names[i];
                        if (i < len)
                        {
                            PropertyInfo pi = obj.GetType().GetProperty(curr);
                            if (pi != null)
                            {
                                obj = pi.GetValue(obj, null);
                                continue;
                            }
                            else
                            {
                                FieldInfo fi = o.GetType().GetField(curr);
                                if (fi != null)
                                {
                                    obj = fi.GetValue(obj);
                                }
                                else
                                {
                                    throw new Exception(string.Format("Property or Field '{0}' cannot be found.", curr));
                                }
                            }

                        }
                        else
                        {
                            MethodInfo mi = null;
                            try
                            {
                                mi = obj.GetType().GetMethod(curr, argstypes);
                            }
                            catch
                            {
                                throw new Exception(string.Format("Method '{0}' not found that suits the specified parameters."));
                            }
                            if (mi != null)
                            {
                                object result = mi.Invoke(obj, param);
                                if (mi.ReturnType != null)
                                {
                                    sfunc.Temps.Push(result);
                                    return 1;
                                }
                                else
                                {
                                    return 0;
                                }
                            }
                            else
                            {
                                throw new Exception(string.Format("Method '{0}' cannot be found.", curr));
                            }

                        }

                    }

                }

            }

            throw new NullReferenceException(string.Format("Reference object '{0}' cannot be null.", curr));

        }

    }

    internal class CmdAssign : Cmd
    {

        public List<string> Names = null;

        public CmdAssign(List<string> names)
        {
            this.Names = names;
        }

        public override int Eval(ScriptFunction sfunc)
        {
            int i = 0;
            foreach (Cmd c in this.Children)
            {
                i += c.Eval(sfunc);
            }
            if (i == 1)
            {
                this.SetProperty(sfunc, sfunc.Temps.Pop());
                this.RemoveFromTemp(sfunc, --i);
            }
            return i;
        }

        private void SetProperty(ScriptFunction sfunc, object val)
        {

            string curr = this.Names[0];
            object o;
            if (sfunc.Objects.TryGetValue(curr, out o))
            {

                if (this.Names.Count == 1)
                {
                    sfunc.Objects[curr] = val;
                    return;
                }
                else
                {

                    object obj = o;

                    int len = this.Names.Count - 1;
                    for (int i = 1; i < this.Names.Count; i++)
                    {
                        curr = this.Names[i];
                        PropertyInfo pi = obj.GetType().GetProperty(curr);
                        if (pi != null)
                        {
                            if (i < len)
                            {
                                obj = pi.GetValue(obj, null);
                                continue;
                            }
                            else
                            {
                                try
                                {
                                    pi.SetValue(obj, val, null);
                                    return;
                                }
                                catch
                                {
                                    throw new Exception(string.Format("Cannot assign value to property '{0}'.", curr));
                                }
                            }
                        }
                        else
                        {
                            FieldInfo fi = o.GetType().GetField(curr);
                            if (fi != null)
                            {
                                if (i < len)
                                {
                                    obj = fi.GetValue(obj);
                                    continue;
                                }
                                else
                                {
                                    try
                                    {
                                        fi.SetValue(obj, val);
                                    }
                                    catch
                                    {
                                        throw new Exception(string.Format("Cannot assign value tp field '{0}'.", curr));
                                    }
                                    return;
                                }
                            }
                            else
                            {
                                throw new Exception(string.Format("Property or Field '{0}' cannot be found.", curr));
                            }
                        }

                    }

                }

            }
            else if (this.Names.Count == 1)
            {
                sfunc.Objects.Add(curr, val);
                return;
            }

            throw new NullReferenceException(string.Format("Reference object '{0}' cannot be null.", curr));

        }

    }

    internal class CmdReturn : Cmd
    {

        public bool Returning = false;

        public CmdReturn(bool returning)
        {
            this.Returning = returning;
        }

        public override int Eval(ScriptFunction sfunc)
        {
            sfunc.Returned = true;
            int i = 0;
            if (this.Returning)
            {
                foreach (Cmd c in this.Children)
                {
                    i += c.Eval(sfunc);
                }
                sfunc.ReturnValue = sfunc.Temps.Pop();
                this.RemoveFromTemp(sfunc, --i);
            }
            return 0;
        }

    }

    internal class CmdProp : Cmd
    {

        public bool Returning = false;

        public CmdProp(bool returning)
        {
            this.Returning = returning;
        }

        public override int Eval(ScriptFunction sfunc)
        {
            sfunc.Returned = true;
            int i = 0;
            if (this.Returning)
            {
                foreach (Cmd c in this.Children)
                {
                    i += c.Eval(sfunc);
                }
                sfunc.ReturnValue = sfunc.Temps.Pop();
                this.RemoveFromTemp(sfunc, --i);
            }
            return 0;
        }

    }

    internal class CmdBool : Cmd
    {

        private enum Operaton
        {
            None,
            EQ,
            NEQ,
            LT,
            GT,
            LTE,
            GTE,
            AND,
            OR,
            NOT,
            XOR
        };

        public bool Value = false;
        private bool Evaluated = false;
        private Operaton Op = Operaton.None;

        public CmdBool(Cmd child)
        {
            this.Children.Add(child);
        }

        public CmdBool(string line, Cmd left, Cmd right)
        {
            this.Op = this.GetOperation(line);
            this.Children.Add(left);
            this.Children.Add(right);
        }

        public CmdBool(string line, Cmd child)
        {
            this.Op = this.GetOperation(line);
            this.Children.Add(child);
        }

        private Operaton GetOperation(string line)
        {
            switch (line)
            {
                case "==":
                    return Operaton.EQ;
                case "!=":
                    return Operaton.NEQ;
                case "<":
                    return Operaton.LT;
                case ">":
                    return Operaton.GT;
                case "<=":
                    return Operaton.LTE;
                case ">=":
                    return Operaton.GTE;
                case "&&":
                    return Operaton.AND;
                case "||":
                    return Operaton.OR;
                case "^":
                    return Operaton.XOR;
                case "!":
                    return Operaton.NOT;
                default:
                    return Operaton.None;
            }
        }

        public CmdBool(bool evaluated, bool val = false)
        {
            this.Evaluated = evaluated;
            this.Value = val;
        }

        public override int Eval(ScriptFunction sfunc)
        {
            if (this.Evaluated)
            {
                sfunc.Temps.Push(this.Value);
                return 1;
            }
            else
            {

                int i = 0;

                foreach (Cmd c in this.Children)
                {
                    i += c.Eval(sfunc);
                }

                if (i == 1 && this.Op == Operaton.NOT)
                {
                    object obj = sfunc.Temps.Pop();
                    if (obj is bool)
                    {
                        sfunc.Temps.Push(!(bool)obj);
                        return i;
                    }
                    else
                    {
                        throw new Exception(string.Format("The operator '{0}' cannot be applied on the value '{1}'.", this.Op.ToString(), obj));
                    }
                }
                else if (i == 2)
                {

                    // take the values from stack in reverse order
                    object o2 = sfunc.Temps.Pop();
                    object o1 = sfunc.Temps.Pop();
                    i -= 1;

                    #region "Switch Op"

                    switch (this.Op)
                    {

                        case Operaton.AND:
                        case Operaton.OR:
                        case Operaton.XOR:
                            
                            if (o1 is bool && o2 is bool)
                            {
                                switch (this.Op)
                                {
                                    case Operaton.AND:
                                        sfunc.Temps.Push((bool)o1 && (bool)o2);
                                        return i;

                                    case Operaton.OR:
                                        sfunc.Temps.Push((bool)o1 || (bool)o2);
                                        return i;

                                    case Operaton.XOR:
                                        sfunc.Temps.Push((bool)o1 ^ (bool)o2);
                                        return i;
                                    
                                }

                            }
                            else
                            {
                                throw new Exception(string.Format("The operator '{0}' cannot be applied on the two values '{1}' and '{2}'.", this.Op.ToString(), o1, o2));
                            }

                            break;

                        default:

                            IComparable obj1 = o1 as IComparable;
                            IComparable obj2 = o2 as IComparable;
                            
                            if (obj1 != null && obj2 != null)
                            {
                                switch (this.Op)
                                {
                                    case Operaton.EQ:
                                        sfunc.Temps.Push(obj1 == obj2);
                                        return i;

                                    case Operaton.NEQ:
                                        sfunc.Temps.Push(obj1 != obj2);
                                        return i;

                                    case Operaton.LT:
                                        try
                                        {
                                            sfunc.Temps.Push(obj1.CompareTo(Convert.ChangeType(obj2, obj1.GetType())) < 0);
                                        }
                                        catch
                                        {
                                            sfunc.Temps.Push(obj2.CompareTo(Convert.ChangeType(obj1, obj2.GetType())) >= 0);

                                        }
                                        return i;

                                    case Operaton.GT:
                                        try
                                        {
                                            sfunc.Temps.Push(obj1.CompareTo(Convert.ChangeType(obj2, obj1.GetType())) > 0);
                                        }
                                        catch
                                        {
                                            sfunc.Temps.Push(obj2.CompareTo(Convert.ChangeType(obj1, obj2.GetType())) <= 0);

                                        }
                                        return i;

                                    case Operaton.LTE:
                                        try
                                        {
                                            sfunc.Temps.Push(obj1.CompareTo(Convert.ChangeType(obj2, obj1.GetType())) <= 0);
                                        }
                                        catch
                                        {
                                            sfunc.Temps.Push(obj2.CompareTo(Convert.ChangeType(obj1, obj2.GetType())) > 0);

                                        }
                                        return i;

                                    case Operaton.GTE:
                                        try
                                        {
                                            sfunc.Temps.Push(obj1.CompareTo(Convert.ChangeType(obj2, obj1.GetType())) >= 0);
                                        }
                                        catch
                                        {
                                            sfunc.Temps.Push(obj2.CompareTo(Convert.ChangeType(obj1, obj2.GetType())) < 0);

                                        }
                                        return i;
                                }

                            }
                            else
                            {
                                throw new Exception(string.Format("The operator '{0}' cannot be applied on the two values '{1}' and '{2}'.", this.Op.ToString(), o1, o2));
                            }
                      
                            break;

                    }

                    #endregion

                }
 
                throw new Exception(string.Format("Error evaluating the two values in the operation {0}.", this.Op.ToString()));

            }
        }

    }

    internal class CmdIf : Cmd
    {

        private bool evaluate = false;
        public virtual bool Evaluate
        {
            get { return this.evaluate; }
        }

        public CmdBool Condition = null;
        public CmdElse Else = null;

        public CmdIf(CmdBool cond)
        {
            this.Condition = cond;
        }

        public override int Eval(ScriptFunction sfunc)
        {
            if (this.Condition.Eval(sfunc) == 1)
            {
                this.evaluate = (bool)sfunc.Temps.Pop();
            }
            return 0;
        }

        public CmdIf GetParentIf()
        {
            return (this.Else != null) ? this.Else : this;
        }

    }

    internal class CmdElse : CmdIf
    {

        private CmdIf IfCmd = null;

        public override bool Evaluate
        {
            get { return !this.IfCmd.Evaluate; }
        }

        public override bool Execute
        {
            get { return false; }
        }

        public CmdElse(CmdIf ifcmd)
            : base(null)
        {
            this.IfCmd = ifcmd;
        }

        public override int Eval(ScriptFunction sfunc)
        {
            return 0;
        }

    }

    internal class CmdEndIf : Cmd
    {

        public CmdIf IfCmd = null;

        public override bool Execute
        {
            get { return false; }
        }

        public CmdEndIf(CmdIf cif)
        {
            this.IfCmd = cif;
        }

        public override int Eval(ScriptFunction sfunc)
        {
            return 0;
        }

    }

}
